package org.lestr.secuel.m3ds.sauvegarde;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Scanner;
import java.util.zip.ZipInputStream;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;

import org.lestr.secuel.m3ds.Camera;
import org.lestr.secuel.m3ds.Univers;
import org.lestr.secuel.m3ds.IUsine;
import org.lestr.secuel.m3ds.Valeur;
import org.lestr.secuel.m3ds.Variable;
import org.lestr.secuel.m3ds.animations.AnimationObjets3D;
import org.lestr.secuel.m3ds.animations.CleObjet3D;
import org.lestr.secuel.m3ds.lumieres.Ampoule;
import org.lestr.secuel.m3ds.lumieres.Lumiere;
import org.lestr.secuel.m3ds.lumieres.Spot;
import org.lestr.secuel.m3ds.maths.IPoint2d;
import org.lestr.secuel.m3ds.maths.ICoordonnees3d;
import org.lestr.secuel.m3ds.objets2D.IImage;
import org.lestr.secuel.m3ds.objets3D.Materiau;
import org.lestr.secuel.m3ds.objets3D.IObjet3d;
import org.lestr.secuel.m3ds.objets3D.IApparence;
import org.lestr.secuel.m3ds.objets3D.ITriangle;


public class M3DSLecteur {
	
	
	// Définition de constantes
	public static final Namespace espaceDeNommage = Namespace.getNamespace("scl", "secuel::m3ds");
	
	
	public static Univers lireUnivers(File fichier, IUsine usine) throws M3DSLectureException {
		
		// Retourne le résultat
		M3DSLecteur lecteur = new M3DSLecteur();
		lecteur.setUsine(usine);
		return lecteur.compilerUnivers(lire(fichier, usine));
		
	}// FIN lireUnivers
	
	
	public static IObjet3d lireObjet3D(File fichier, IUsine usine) throws M3DSLectureException {
		
		// Retourne le résultat
		M3DSLecteur lecteur = new M3DSLecteur();
		lecteur.setUsine(usine);
		return lecteur.compilerObjet3D(lire(fichier, usine));
		
	}// FIN lireUnivers
	
	
	public static Element lire(File fichier, IUsine usine){
		
		// Variables locales
		Document document = null;
		
		// Ouverture du document
		try {
			
			// Récupère le flux compressé
			ZipInputStream flux = new ZipInputStream(new FileInputStream(fichier));
			flux.getNextEntry();
			
			// Ouvre le document
			document = new SAXBuilder().build(flux);
			
			// Ferme le flux
			flux.closeEntry();
			flux.close();
			
		} catch (JDOMException e) {} 
		catch (IOException e) {}
		
		// Retourne le résultat
		return document.getRootElement();
		
	}// FIN lire
	
	
	// Déclaration des attributs
	private IUsine usine;
	private Hashtable<String, ITriangle> triangles;
	
	
	/** Constructeur */
	public M3DSLecteur(){
		
		// Instanciation des attributs
		usine = null;
		triangles = null;
		
	}// FIN Constructeur
	
	
	// Accesseurs
	public IUsine getUsine() {
		return usine;
	}
	public void setUsine(IUsine usine) {
		this.usine = usine;
	}
	
	
	/** Compile un univers */
	public Univers compilerUnivers(Element élément) throws M3DSLectureException {
		
		// Variables locales
		Univers rslt = usine.univers();
		
		// Mémorise les paramètres généraux
		rslt.setNom(élément.getAttributeValue("nom"));
		
		// Parcours le XML des objets 3D de l'univers
		for(Object objet3DXML : élément.getChildren("objet3D", espaceDeNommage))
			rslt.ajoutObjet3D(compilerObjet3D((Element) objet3DXML));
		
		// Parcours le XML des lumières de l'univers
		for(Object lumière3DXML : élément.getChildren("lumière", espaceDeNommage))
			rslt.ajoutLumière(compilerLumière((Element) lumière3DXML));
		
		// Parcours le XML des caméras de l'univers
		for(Object caméraXML : élément.getChildren("caméra", espaceDeNommage))
			rslt.ajoutCaméra(compilerCaméra((Element) caméraXML));
		
		// TODO
		
		// Mémorise les variables
		for(Object variableXML : élément.getChildren("variable", espaceDeNommage))
			rslt.getVariables().add(compilerVariable((Element) variableXML));
		
		// Retourne le résultat
		return rslt;
		
	}// FIN compilerUnivers
	
	
	/** Compile une caméra */
	public Camera compilerCaméra(Element élément) throws M3DSLectureException {
		
		// Variables locales
		Camera rslt = usine.caméra();
		
		// Mémorise les paramètres généraux de la lumière
		rslt.setNom(élément.getAttributeValue("nom", espaceDeNommage));
		rslt.setX(Float.parseFloat(élément.getAttributeValue("x", espaceDeNommage)));
		rslt.setY(Float.parseFloat(élément.getAttributeValue("y", espaceDeNommage)));
		rslt.setZ(Float.parseFloat(élément.getAttributeValue("z", espaceDeNommage)));
		rslt.setAngleX(Float.parseFloat(élément.getAttributeValue("angleX", espaceDeNommage)));
		rslt.setAngleY(Float.parseFloat(élément.getAttributeValue("angleY", espaceDeNommage)));
		rslt.setAngleZ(Float.parseFloat(élément.getAttributeValue("angleZ", espaceDeNommage)));
		
		// Retourne le résultat
		return rslt;
		
	}// FIN compilerCaméra
	
	
	/** Compile une lumière */
	public Lumiere compilerLumière(Element élément) throws M3DSLectureException {
		
		// Variables locales
		Lumiere rslt = null;
		
		// Récupère le type de la lumière
		String type = élément.getAttributeValue("type", espaceDeNommage);
		
		// Si c'est une lumière ambiante
		if(type.equals("ambiante")){
			
			// Instancie la lumière
			rslt = usine.lumièreAmbiante();
			
			
		}
		
		// Si c'est une ampoule
		else if(type.equals("ampoule")){
			
			// Instancie la lumière
			rslt = usine.ampoule();
			
			// Mémorise les paramètres
			rslt.setNom(élément.getAttributeValue("nom", espaceDeNommage));
			((Ampoule) rslt).setPortée(Float.parseFloat(élément.getAttributeValue("portée", espaceDeNommage)));
			
		}
		
		// Si c'est un spot
		else if(type.equals("spot")){
			
			// Instancie la lumière
			rslt = usine.spot();
			
			// Mémorise les paramètres
			rslt.setNom(élément.getAttributeValue("nom"));
			((Spot) rslt).setPortée(Float.parseFloat(élément.getAttributeValue("portée", espaceDeNommage)));
			((Spot) rslt).setAngleX(Float.parseFloat(élément.getAttributeValue("angleX", espaceDeNommage)));
			((Spot) rslt).setAngleY(Float.parseFloat(élément.getAttributeValue("angleY", espaceDeNommage)));
			((Spot) rslt).setAngleZ(Float.parseFloat(élément.getAttributeValue("angleZ", espaceDeNommage)));
			
		}
		
		// Mémorise les paramètres généraux de la lumière
		rslt.setNom(élément.getAttributeValue("nom", espaceDeNommage));
		rslt.setX(Float.parseFloat(élément.getAttributeValue("x", espaceDeNommage)));
		rslt.setY(Float.parseFloat(élément.getAttributeValue("y", espaceDeNommage)));
		rslt.setZ(Float.parseFloat(élément.getAttributeValue("z", espaceDeNommage)));
		rslt.setIntensité(Integer.parseInt(élément.getAttributeValue("intensité", espaceDeNommage)));
		rslt.setAllumée(élément.getAttributeValue("estAllumé", espaceDeNommage).equals("true"));
		rslt.setCouleur(Integer.parseInt(élément.getAttributeValue("rouge", espaceDeNommage)),
					    Integer.parseInt(élément.getAttributeValue("vert", espaceDeNommage)),
					    Integer.parseInt(élément.getAttributeValue("bleu", espaceDeNommage)));
		
		// Mémorise les variables
		for(Object variableXML : élément.getChildren("variable", espaceDeNommage))
			rslt.getVariables().add(compilerVariable((Element) variableXML));
		
		// Retourne le résultat
		return rslt;
		
	}// FIN compilerLumière
	
	
	/** Compile un objet3D */
	public IObjet3d compilerObjet3D(Element élément) throws M3DSLectureException {
		
		// Variables locales
		IObjet3d rslt = usine.objet3D();
		
		// Si on est en mode éclaté
		if(élément.getAttribute("import", espaceDeNommage) != null){
			
			// Lit le fichier importé
			// TODO rslt = lireObjet3D(new File(racine + élément.getAttributeValue("import")));
			
		}
		
		// Sinon
		else{
			
			// Si fichier exclusif
			if(élément.getAttributeValue("fichierExclusif", espaceDeNommage).equals("true")){
				
				// Charge le fichier
				rslt.setFichier(new File(élément.getAttributeValue("fichier", espaceDeNommage)));
				
			}
			
			// Sinon
			else{
				
				// Initialise les triangles
				triangles = new Hashtable<String, ITriangle>();
				
				// Parcours les triangles
				Element trianglesXML = élément.getChild("triangles", espaceDeNommage);
				Collection<ITriangle> triangles = new ArrayList<ITriangle>();
				for(Object triangleXML : trianglesXML.getChildren("triangle", espaceDeNommage))
					triangles.add(compilerTriangle((Element) triangleXML));
				rslt.ajouterTriangles(triangles);
				
				// Mémorise la texture
				compilerTexture(élément.getChild("texture", espaceDeNommage), rslt.obtenirApparence());
				
			}
		
			// Mémorise les paramètres généraux
			rslt.définirNom(élément.getAttributeValue("nom", espaceDeNommage));
			rslt.setX(Float.parseFloat(élément.getAttributeValue("x", espaceDeNommage)));
			rslt.setY(Float.parseFloat(élément.getAttributeValue("y", espaceDeNommage)));
			rslt.setZ(Float.parseFloat(élément.getAttributeValue("z", espaceDeNommage)));
			rslt.setAngleX(Float.parseFloat(élément.getAttributeValue("angleX", espaceDeNommage)));
			rslt.setAngleY(Float.parseFloat(élément.getAttributeValue("angleY", espaceDeNommage)));
			rslt.setAngleZ(Float.parseFloat(élément.getAttributeValue("angleZ", espaceDeNommage)));
			rslt.setDimensionX(Float.parseFloat(élément.getAttributeValue("dimensionX", espaceDeNommage)));
			rslt.setDimensionY(Float.parseFloat(élément.getAttributeValue("dimensionY", espaceDeNommage)));
			rslt.setDimensionZ(Float.parseFloat(élément.getAttributeValue("dimensionZ", espaceDeNommage)));
			rslt.définirTransparence(Integer.parseInt(élément.getAttributeValue("transparence", espaceDeNommage)));
			rslt.définirTransparence(Integer.parseInt(élément.getAttributeValue("réflexion", espaceDeNommage)));
			rslt.définirSolide(élément.getAttributeValue("solide", espaceDeNommage).equals("true"));
			
			// Mémorise les animations
			Element animationsXML = élément.getChild("animations", espaceDeNommage);
			if(animationsXML != null)
				for(Object animationXML : animationsXML.getChildren("animation", espaceDeNommage))
					rslt.ajoutAnimation(compilerAnimationObjet3D((Element) animationXML));
			
			// Mémorise les variables
			for(Object variableXML : élément.getChildren("variable", espaceDeNommage))
				rslt.obtenirVariables().add(compilerVariable((Element) variableXML));
			
		}
		
		// Retourne le résultat
		return rslt;
		
	}// FIN compilerScène
	
	
	/** Compile une texture */
	public void compilerTexture(Element élément, IApparence texture) throws M3DSLectureException {
		
		// Mémorise les paramètres généraux
		texture.setX(Float.parseFloat(élément.getAttributeValue("x", espaceDeNommage)));
		texture.setY(Float.parseFloat(élément.getAttributeValue("y", espaceDeNommage)));
		texture.setDimensionX(Float.parseFloat(élément.getAttributeValue("dimensionX", espaceDeNommage)));
		texture.setDimensionY(Float.parseFloat(élément.getAttributeValue("dimensionY", espaceDeNommage)));
		compilerMatériau(élément.getChild("matériau", espaceDeNommage), texture.obtenirMatériauStandard());
		
		// Normales
		texture.définirNormalesAutomatiques(élément.getAttributeValue("autoNormales", espaceDeNommage).equals("true"));
		if(!texture.sontNormalesAutomatiques()){
			
			// Initialise la liste des normales
			texture.setNormales(new ArrayList<ICoordonnees3d>());
			
			// Récupère le XML des normales
			Element normales = élément.getChild("normales", espaceDeNommage);
			
			// Compile et mémorise les normales
			for(Object point2DXML : normales.getChildren("point3D", espaceDeNommage))
				texture.obtenirNormales().add(compilerPoint3D((Element) point2DXML));
		}
		
		// Coordonnées de texture
		texture.définirTexturageAutomatique(élément.getAttributeValue("autoTexturé", espaceDeNommage).equals("true"));
		if(!texture.estTexturageAutomatique()){
			
			// Initialise la liste des coordonnées
			texture.setCoordonnées(new ArrayList<IPoint2d>());
			
			// Récupère le XML des coordonnées
			Element coordonnées = élément.getChild("coordonnées", espaceDeNommage);
			
			// Compile et mémorise les coordonnées
			for(Object point2DXML : coordonnées.getChildren("point2D", espaceDeNommage))
				texture.obtenirCoordonnées().add(compilerPoint2D((Element) point2DXML));
			
		}
		
		// Parcours matériaux spécifiques
		for(Object materiauXML : élément.getChildren("matériauSpécifique", espaceDeNommage)){
			
			// Récupère l'identifiant du triangle
			String triangleID = ((Element) materiauXML).getAttributeValue("triangleID", espaceDeNommage);
			
			// Récupère le triangle correspondant
			ITriangle triangle = triangles.get(triangleID);
			
			// Si le triangle a été trouvé, lui associe un matériau
			if(triangle != null){
				
				// Compile le matériau
				Materiau matériau = usine.matériau();
				compilerMatériau(((Element) materiauXML).getChild("matériau", espaceDeNommage), matériau);
				
				// Effectue l'association
				texture.définirMatériau(matériau, triangle);
				
			}
			
		}
		
	}// FIN compilerTexture
	
	
	/** Compile un matériau */
	public void compilerMatériau(Element élément, Materiau matériau){
				
		// Mémorise les paramètres du matériau
		matériau.définirCouleurAmbiante(Float.parseFloat(élément.getAttributeValue("rougeAmbiant", espaceDeNommage)), 
									Float.parseFloat(élément.getAttributeValue("vertAmbiant", espaceDeNommage)), 
									Float.parseFloat(élément.getAttributeValue("bleuAmbiant", espaceDeNommage)));
		matériau.définirCouleurDiffuse(Float.parseFloat(élément.getAttributeValue("rougeDiffue", espaceDeNommage)), 
								   Float.parseFloat(élément.getAttributeValue("vertDiffue", espaceDeNommage)), 
								   Float.parseFloat(élément.getAttributeValue("bleuDiffue", espaceDeNommage)));
		matériau.définirCouleurSpéculaire(Float.parseFloat(élément.getAttributeValue("rougeSpéculaire", espaceDeNommage)), 
				   					  Float.parseFloat(élément.getAttributeValue("vertSpéculaire", espaceDeNommage)), 
				   					  Float.parseFloat(élément.getAttributeValue("bleuSpéculaire", espaceDeNommage)));
		matériau.définirCouleurDiffuse(Float.parseFloat(élément.getAttributeValue("rougeEmissif", espaceDeNommage)), 
				   				   Float.parseFloat(élément.getAttributeValue("vertEmissif", espaceDeNommage)), 
				   				   Float.parseFloat(élément.getAttributeValue("bleuEmissif", espaceDeNommage)));
		
		// S'il y a une image, la compile et la méorise
		if(élément.getChild("image", espaceDeNommage) != null)
			matériau.définirImage(compilerImage(élément.getChild("image", espaceDeNommage)));
		
	}// FIN compilerMatériau
	
	
	/** Compile une image */
	public IImage compilerImage(Element élément) {
		
		// Variables locales
		IImage rslt;
		
		// Récupère la largeur et la hauteur
		int largeur = Integer.parseInt(élément.getAttributeValue("largeur", espaceDeNommage));
		int hauteur = Integer.parseInt(élément.getAttributeValue("hauteur", espaceDeNommage));
		
		// Instancie l'image
		rslt = usine.getImage(largeur, hauteur);
		
		// Récupère les pixels
		Scanner scan = new Scanner(élément.getText());
		for(int i = 0 ; i <= largeur - 1 ; i = i + 1)
			for(int j = 0 ; j <= hauteur - 1 ; j = j + 1){
				rslt.obtenirPixel(i, j).setCouleur(scan.nextInt(), scan.nextInt(), scan.nextInt());
				rslt.définirTransparence(i, j, scan.nextInt() / 255);
			}
		
		// Retourne le résultat
		return rslt;
		
	}// FIN compilerImage


	/** Compile une animation */
	public AnimationObjets3D compilerAnimationObjet3D(Element élément) throws M3DSLectureException {
		
		// Variables locales
		AnimationObjets3D rslt = usine.animationObjet3D();
		
		// Mémorise les paramètres généraux
		rslt.setNom(élément.getAttributeValue("nom", espaceDeNommage));
		
		// Mémorise les clés
		for(Object cléXML : élément.getChildren("clé", espaceDeNommage))
			rslt.getClés().add(compilerCléObjet3D((Element) cléXML));
		
		// Mémorise les variables
		for(Object variableXML : élément.getChildren("variable", espaceDeNommage))
			rslt.getVariables().add(compilerVariable((Element) variableXML));
		
		// Retourne le résultat
		return rslt;
		
	}// FIN compilerAnimation
	
	
	/** Compile une clé */
	public CleObjet3D compilerCléObjet3D(Element élément) throws M3DSLectureException {
		
		// Variables locales
		CleObjet3D rslt = usine.cléObjet3D();
		
		// Mémorise les paramètres généraux
		rslt.setX(Float.parseFloat(élément.getAttributeValue("x", espaceDeNommage)));
		rslt.setY(Float.parseFloat(élément.getAttributeValue("y", espaceDeNommage)));
		rslt.setZ(Float.parseFloat(élément.getAttributeValue("z", espaceDeNommage)));
		rslt.setAngleX(Float.parseFloat(élément.getAttributeValue("angleX", espaceDeNommage)));
		rslt.setAngleY(Float.parseFloat(élément.getAttributeValue("angleY", espaceDeNommage)));
		rslt.setAngleZ(Float.parseFloat(élément.getAttributeValue("angleZ", espaceDeNommage)));
		rslt.setDimensionX(Float.parseFloat(élément.getAttributeValue("dimensionX", espaceDeNommage)));
		rslt.setDimensionY(Float.parseFloat(élément.getAttributeValue("dimensionY", espaceDeNommage)));
		rslt.setDimensionZ(Float.parseFloat(élément.getAttributeValue("dimensionZ", espaceDeNommage)));
		
		// Retourne le résultat
		return rslt;
		
	}// FIN compilerClé
	
	
	/** Compile une variable */
	private Variable compilerVariable(Element élément) throws M3DSLectureException {
		
		// Variables locales
		Variable rslt = new Variable();
		
		// Mémorise les paramètres généraux
		rslt.setNom(élément.getAttributeValue("nom", espaceDeNommage));
		rslt.setValeur(compilerValeur(élément.getChild("valeur", espaceDeNommage)));
		
		// Retourne le résultat
		return rslt;
		
	}// FIN compilerVariable
	
	
	/** Compile une valeur */
	private Valeur compilerValeur(Element élément) throws M3DSLectureException {
		
		// Valeurs locales
		Valeur rslt = new Valeur();
		
		// Récupère le type de la variable
		int type = Integer.parseInt(élément.getAttributeValue("type", espaceDeNommage));
		
		// Mémorise ses données en fonction du type
		switch(type){
		
		case Valeur.DOUBLE:
			rslt.setDouble(Double.parseDouble(élément.getText()));
			break;
		
		case Valeur.FLOAT:
			rslt.setFloat(Float.parseFloat(élément.getText()));
			break;
		
		case Valeur.INT:
			rslt.setInt(Integer.parseInt(élément.getText()));
			break;
		
		case Valeur.STRING:
			rslt.setString(élément.getText());
			break;
		
		}
		
		// Retourne le résultat
		return rslt;
		
	}// FIN compilerValeur
	
	
	/** Compile un triangle */
	public ITriangle compilerTriangle(Element élément) throws M3DSLectureException {
		
		// Variables locales
		ITriangle rslt = usine.triangle();
		
		// Mémorise les 3 points du triangles
		rslt.setSommet1(compilerPoint3D((Element) élément.getChildren("point3D", espaceDeNommage).get(0)));
		rslt.setSommet2(compilerPoint3D((Element) élément.getChildren("point3D", espaceDeNommage).get(1)));
		rslt.setSommet3(compilerPoint3D((Element) élément.getChildren("point3D", espaceDeNommage).get(2)));
		
		// Mémorise l'ID du triangle
		triangles.put(élément.getAttributeValue("ID", espaceDeNommage), rslt);
		
		// Retourne le résultat
		return rslt;
		
	}// FIN compilerTriangle
	
	
	/** Compile un nouvellesPosition */
	public ICoordonnees3d compilerPoint3D(Element élément) throws M3DSLectureException {
		
		// Variables locales
		ICoordonnees3d rslt = usine.nouvellesCoordonnées();
		
		// Mémorise les paramètres du point
		rslt.définirX(Float.parseFloat(élément.getAttributeValue("x", espaceDeNommage)));
		rslt.définirY(Float.parseFloat(élément.getAttributeValue("y", espaceDeNommage)));
		rslt.définirZ(Float.parseFloat(élément.getAttributeValue("z", espaceDeNommage)));
		
		// Retourne le résultat
		return rslt;
		
	}// FIN compilerPoint3D
	
	
	/** Compile un point2d */
	public IPoint2d compilerPoint2D(Element élément) throws M3DSLectureException {
		
		// Variables locales
		IPoint2d rslt = usine.point2d();
		
		// Mémorise les paramètres du point
		rslt.définirX(Float.parseFloat(élément.getAttributeValue("x", espaceDeNommage)));
		rslt.définirY(Float.parseFloat(élément.getAttributeValue("y", espaceDeNommage)));
		
		// Retourne le résultat
		return rslt;
		
	}// FIN compilerPoint2D
	

}// FIN Classe
